using System;
using Resource = Velocity4Net.Runtime.ResourceManage.Resource;
using StringUtils = Velocity4Net.Util.StringUtils;
using MethodInvocationException = Velocity4Net.Errors.MethodInvocationException;
using ResourceNotFoundException = Velocity4Net.Errors.ResourceNotFoundException;

using Velocity4Net.Ctx;
using Velocity4Net.Runtime.Parse.AST;
using System.IO;
using Velocity4Net.Runtime.Parse;
using Velocity4Net.Errors;
using Velocity4Net.App.Events;
using Velocity4Net.Runtime.Logging;

namespace Velocity4Net.Runtime.Directives
{
    /**
     * <p>Pluggable directive that handles the #include() statement in VTL.
     * This #include() can take multiple arguments of either
     * StringLiteral or Reference.</p>
     *
     * <p>Notes:</p>
     * <ol>
     * <li>For security reasons, the included source material can only come
     *    from somewhere within the template root tree.  If you want to include
     *    content from elsewhere on your disk, add extra template roots, or use
     *    a link from somwhere under template root to that content.</li>
     *
     *  <li>By default, there is no output to the render stream in the event of
     *    a problem.  You can override this behavior with two property values :
     *       include.output.errormsg.start
     *       include.output.errormsg.end
     *     If both are defined in velocity.properties, they will be used to
     *     in the render output to bracket the arg string that caused the
     *     problem.
     *     Ex. : if you are working in html then
     *       include.output.errormsg.start=&lt;!-- #include error :
     *       include.output.errormsg.end= --&gt;
     *     might be an excellent way to start...</li>
     *
     *  <li>As noted above, #include() can take multiple arguments.
     *    Ex : #include('foo.vm' 'bar.vm' $foo)
     *    will include all three if valid to output without any
     *    special separator.</li>
     *  </ol>
     *
     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
     * @author <a href="mailto:kav@kav.dk">Kasper Nielsen</a>
     * @version $Id: Include.java 746438 2009-02-21 05:41:24Z nbubna $
     */
    public class Include : InputBase
    {
        private String outputMsgStart = "";
        private String outputMsgEnd = "";

        /**
         * Return name of this directive.
         * @return The name of this directive.
         */
        public override String Name
        {
            get
            {
                return "include";
            }
            set { }
        }

        /**
         * Return type of this directive.
         * @return The type of this directive.
         */
        public override int Type
        {
            get
            {
                return 2;
            }
        }

        /**
         * Since there is no processing of content,
         * there is never a need for an internal scope.
         */
        public override bool ScopeProvided
        {
            get
            {
                return false;
            }
        }

        /**
         *  simple init - init the tree and get the elementKey from
         *  the AST
         * @param rs
         * @param context
         * @param node
         * @throws TemplateInitException
         */
        public void init(RuntimeServices rs, InternalContextAdapter context,
                         INode node)
        {
            base.Init(rs, context, node);

            /*
             *  get the msg, and add the space so we don't have to
             *  do it each time
             */
            outputMsgStart = rsvc.GetString(RuntimeConstants.ERRORMSG_START);
            outputMsgStart = outputMsgStart + " ";

            outputMsgEnd = rsvc.GetString(RuntimeConstants.ERRORMSG_END);
            outputMsgEnd = " " + outputMsgEnd;
        }

        /**
         *  iterates through the argument list and renders every
         *  argument that is appropriate.  Any non appropriate
         *  arguments are logged, but render() continues.
         * @param context
         * @param writer
         * @param node
         * @return True if the directive rendered successfully.
         * @throws IOException
         * @throws MethodInvocationException
         * @throws ResourceNotFoundException
         */
        public override bool Render(InternalContextAdapter context,
                               TextWriter writer, INode node)
        {
            /*
             *  get our arguments and check them
             */

            int argCount = node.jjtGetNumChildren();

            for (int i = 0; i < argCount; i++)
            {
                /*
                 *  we only handle StringLiterals and References right now
                 */

                INode n = node.jjtGetChild(i);

                if (n.NodeType == ParserTreeConstants.JJTSTRINGLITERAL ||
                     n.NodeType == ParserTreeConstants.JJTREFERENCE)
                {
                    if (!renderOutput(n, context, writer))
                        outputErrorToStream(writer, "error with arg " + i
                            + " please see log.");
                }
                else
                {
                    String msg = "invalid #include() argument '"
                      + n.ToString() + "' at " + Log.formatFileString(this);
                    rsvc.getLog().error(msg);
                    outputErrorToStream(writer, "error with arg " + i
                        + " please see log.");
                    throw new VelocityException(msg);
                }
            }

            return true;
        }

        /**
         *  does the actual rendering of the included file
         *
         *  @param node AST argument of type StringLiteral or Reference
         *  @param context valid context so we can render References
         *  @param writer output Writer
         *  @return boolean success or failure.  failures are logged
         *  @exception IOException
         *  @exception MethodInvocationException
         *  @exception ResourceNotFoundException
         */
        private bool renderOutput(INode node, InternalContextAdapter context,
                                      TextWriter writer)
        {
            if (node == null)
            {
                rsvc.getLog().error("#include() null argument");
                return false;
            }

            /*
             *  does it have a value?  If you have a null reference, then no.
             */
            Object value = node.GetValue(context);
            if (value == null)
            {
                rsvc.getLog().error("#include() null argument");
                return false;
            }

            /*
             *  get the path
             */
            String sourcearg = value.ToString();

            /*
             *  check to see if the argument will be changed by the event handler
             */

            String arg = EventHandlerUtil.includeEvent(rsvc, context, sourcearg, context.CurrentTemplateName, Name);

            /*
             *   a null return value from the event cartridge indicates we should not
             *   input a resource.
             */
            bool blockinput = false;
            if (arg == null)
                blockinput = true;

            Resource resource = null;

            try
            {
                if (!blockinput)
                    resource = rsvc.getContent(arg, getInputEncoding(context));
            }
            catch (ResourceNotFoundException rnfe)
            {
                /*
                 * the arg wasn't found.  Note it and throw
                 */
                rsvc.getLog().error("#include(): cannot find resource '" + arg +
                                    "', called at " + Log.formatFileString(this));
                throw rnfe;
            }

            /**
             * pass through application level runtime exceptions
             */
            catch (ApplicationException e)
            {
                rsvc.getLog().error("#include(): arg = '" + arg +
                                    "', called at " + Log.formatFileString(this));
                throw e;
            }
            catch (System.Exception e)
            {
                String msg = "#include(): arg = '" + arg +
                            "', called at " + Log.formatFileString(this);
                rsvc.getLog().error(msg, e);
                throw new VelocityException(msg, e);
            }


            /*
             *    note - a blocked input is still a successful operation as this is
             *    expected behavior.
             */

            if (blockinput)
                return true;

            else if (resource == null)
                return false;

            writer.Write((String)resource.Data);
            return true;
        }

        /**
         *  Puts a message to the render output stream if ERRORMSG_START / END
         *  are valid property strings.  Mainly used for end-user template
         *  debugging.
         *  @param writer
         *  @param msg
         *  @throws IOException
         */
        private void outputErrorToStream(TextWriter writer, String msg)
        {
            if (outputMsgStart != null && outputMsgEnd != null)
            {
                writer.Write(outputMsgStart);
                writer.Write(msg);
                writer.Write(outputMsgEnd);
            }
        }
    }
}
